Popular Searches
Popular Course Categories
Popular Courses

Solving Multithreading, File Handling, and Debugging Scenarios

Solving Multithreading, File Handling, and Debugging Scenarios

Core Java Projects, Interview Preparation, and Career Readiness

Solving Multithreading, File Handling, and Debugging Scenarios in Java

Course: Core Java Training

Topic: Solving Multithreading, File Handling, and Debugging Scenarios

Course Link: Core Java Training at JustAcademy

Demo Link: Register for a Course Demo

Website: JustAcademy Official Website


1. Introduction

Solving multithreading, file handling, and debugging scenarios is an important part of practical Core Java development. These skills help programmers create applications that perform multiple tasks, work with external data, manage system resources, and identify errors efficiently.

Multithreading allows multiple threads of execution to make progress within one application. File handling enables a program to read, write, update, and manage information stored in files. Debugging is the systematic process of finding the cause of unexpected behaviour and correcting it.

These topics are connected. For example, a logging application may use multiple threads to process tasks, write information to files, and require debugging when two threads try to update shared data at the same time.

2. Learning Objectives

  • Understand Java threads and common concurrency problems.
  • Create threads using the Thread class and Runnable interface.
  • Use synchronization to protect shared mutable data.
  • Understand race conditions, deadlocks, and thread communication.
  • Read and write files using Java I/O and NIO APIs.
  • Handle file-related exceptions and release resources correctly.
  • Use breakpoints, variable inspection, stack traces, and logging to debug programs.
  • Solve practical scenarios that combine concurrency, file operations, and debugging.

3. Understanding Multithreading in Java

3.1 What Is a Thread?

A thread is an independent sequence of execution within a process. A Java application begins with a main thread, and it can create additional threads to perform work concurrently.

For example, an application might download a file while keeping its user interface responsive. Another application might process several independent data files using a pool of worker threads.

3.2 Process vs Thread

Feature Process Thread
Meaning An executing program with its own process environment A sequence of execution within a process
Memory Normally has its own address space Threads within a process share process memory
Communication Often requires inter-process communication Can communicate through shared objects and coordination mechanisms
Common use Running separate applications or processes Performing concurrent tasks inside an application

3.3 Creating a Thread Using the Thread Class

One way to create a thread is to extend the Thread class and override its run() method. Call start() to schedule the new thread; calling run() directly simply invokes the method on the current thread.

class MessageThread extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 3; i++) {
            System.out.println(
                Thread.currentThread().getName() + ": " + i
            );
        }
    }
}

public class ThreadExample {
    public static void main(String[] args) {
        MessageThread worker = new MessageThread();
        worker.start();

        System.out.println("Main method continues");
    }
}

Possible output:

Main method continues
Thread-0: 1
Thread-0: 2
Thread-0: 3

The exact order is not guaranteed because the main thread and worker thread can be scheduled differently. Your output may show the worker's messages before or after the main message.

3.4 Creating a Thread Using Runnable

The Runnable interface represents a task that can be executed by a thread. It separates the task's logic from the thread object and is commonly preferred when a class already extends another class.

public class RunnableExample {
    public static void main(String[] args) {
        Runnable task = () -> {
            for (int i = 1; i <= 3; i++) {
                System.out.println("Task step: " + i);
            }
        };

        Thread worker = new Thread(task, "Worker-1");
        worker.start();
    }
}

Possible output:

Task step: 1
Task step: 2
Task step: 3

The lambda expression provides a compact implementation of the single-method Runnable interface. This example does not guarantee ordering relative to any other concurrent thread.

3.5 Common Thread Lifecycle States

State Description
NEW The thread has been created but not started.
RUNNABLE The thread is executing or eligible to execute, depending on scheduling.
BLOCKED The thread is waiting to acquire a monitor lock.
WAITING The thread is waiting indefinitely for another action, such as notification or thread completion.
TIMED_WAITING The thread is waiting for a specified period, such as during timed sleep or join.
TERMINATED The thread has completed execution.

Use getState() for diagnostics, but remember that a thread's state can change immediately after it is inspected.

4. Solving Common Multithreading Scenarios

4.1 Scenario: A Shared Counter Produces the Wrong Result

Problem: Two threads increment the same counter. The final result is sometimes smaller than expected.

Cause: The expression count++ is a read-modify-write operation. Without coordination, two threads can read the same old value and overwrite one another's updates. This is a race condition.

Solution using synchronized:

class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

public class CounterExample {
    public static void main(String[] args)
            throws InterruptedException {
        Counter counter = new Counter();

        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) {
                counter.increment();
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(counter.getCount());
    }
}

Output: 20000

Explanation: The synchronized increment method permits only one thread at a time to execute that method while holding the same object's monitor. The calls to join() ensure that the main thread waits for both workers to finish before reading the final value.

Alternative: Use AtomicInteger for a simple atomic counter when more complex locking is unnecessary.

import java.util.concurrent.atomic.AtomicInteger;

AtomicInteger counter = new AtomicInteger();

counter.incrementAndGet();
System.out.println(counter.get());

4.2 Scenario: A Thread Must Wait for Another Thread

Problem: The main thread prints a completion message before the worker has finished.

Solution: Use join() when one thread needs to wait for another thread to terminate.

public class JoinExample {
    public static void main(String[] args)
            throws InterruptedException {
        Thread worker = new Thread(() -> {
            try {
                Thread.sleep(1000);
                System.out.println("Worker completed");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                System.err.println("Worker interrupted");
            }
        });

        worker.start();
        worker.join();

        System.out.println("Main continues after worker completion");
    }
}

Output:

Worker completed
Main continues after worker completion

Important: sleep() pauses the current thread for a period, but it does not release any monitor locks the thread holds. join() waits for another thread to terminate.

4.3 Scenario: A Thread Is Interrupted

Interruption is a cooperative way to signal that a thread should stop waiting or cancel its work. It is not generally appropriate to forcibly terminate a thread.

public class InterruptExample {
    public static void main(String[] args)
            throws InterruptedException {
        Thread worker = new Thread(() -> {
            try {
                while (!Thread.currentThread().isInterrupted()) {
                    System.out.println("Working...");
                    Thread.sleep(300);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                System.out.println("Worker received interruption");
            }
        });

        worker.start();
        Thread.sleep(700);
        worker.interrupt();
        worker.join();

        System.out.println("Worker stopped");
    }
}

The exact number of Working... messages may vary. When an interruptible operation such as sleep() throws InterruptedException, the interrupt status is cleared by that operation. Restoring the status is appropriate when the method cannot fully handle the interruption itself.

4.4 Scenario: Deadlock Between Two Threads

Problem: Two threads each hold one lock and wait for the other lock. Neither can proceed.

Example of a potential deadlock:

Object lockA = new Object();
Object lockB = new Object();

Thread t1 = new Thread(() -> {
    synchronized (lockA) {
        synchronized (lockB) {
            System.out.println("T1 acquired both locks");
        }
    }
});

Thread t2 = new Thread(() -> {
    synchronized (lockB) {
        synchronized (lockA) {
            System.out.println("T2 acquired both locks");
        }
    }
});

t1.start();
t2.start();

This code can deadlock if each thread acquires its first lock before attempting the second. It may also finish without deadlocking, so the bug is timing-dependent.

Solution: Establish one global lock acquisition order and follow it consistently.

Thread t1 = new Thread(() -> {
    synchronized (lockA) {
        synchronized (lockB) {
            System.out.println("T1 completed safely");
        }
    }
});

Thread t2 = new Thread(() -> {
    synchronized (lockA) {
        synchronized (lockB) {
            System.out.println("T2 completed safely");
        }
    }
});

Both threads now acquire lockA before lockB, removing the lock-order cycle shown in the earlier example. In larger systems, reduce nested locking, keep critical sections short, and consider higher-level concurrency utilities.

4.5 Scenario: Multiple Tasks Need a Thread Pool

Creating a new thread for every task can waste resources. An executor manages worker threads and accepts tasks through a standard API.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);

        try {
            for (int i = 1; i <= 5; i++) {
                final int taskId = i;

                executor.submit(() ->
                    System.out.println(
                        "Task " + taskId + " on " +
                        Thread.currentThread().getName()
                    )
                );
            }
        } finally {
            executor.shutdown();
        }
    }
}

Tasks may run in different orders, and the exact worker thread assigned to a task is not guaranteed. Calling shutdown() stops the executor from accepting new tasks while allowing submitted tasks to finish. For production applications, consider bounded queues, task rejection policies, and graceful termination requirements.

5. File Handling in Java

5.1 What Is File Handling?

File handling allows a Java program to store and retrieve information from files and directories. Typical operations include creating a file, reading its content, writing new content, appending data, copying files, and checking whether a path exists.

Java provides traditional I/O classes in java.io and modern file-system utilities in java.nio.file. The Path and Files classes are convenient for many everyday file operations.

5.2 Important File Handling Classes

Class or Interface Purpose
File Represents a file or directory path using the older I/O API.
Path Represents a path in the file system.
Files Provides static methods to read, write, copy, move, and inspect files.
FileReader Reads character data using a character encoding configured by the reader.
FileWriter Writes character data using a character encoding configured by the writer.
BufferedReader Reads text efficiently and supports reading one line at a time.
BufferedWriter Buffers character output and provides methods such as newLine().
InputStream / OutputStream Reads or writes byte-oriented data.

5.3 Create a File and Write Content

The following example writes UTF-8 text to a file. The file is created if it does not exist and replaced if it already exists.

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

public class WriteFileExample {
    public static void main(String[] args) {
        Path path = Path.of("sample.txt");
        String content = "Welcome to Core Java file handling.";

        try {
            Files.writeString(
                path,
                content,
                StandardCharsets.UTF_8
            );

            System.out.println("File written successfully");
        } catch (IOException e) {
            System.err.println("Unable to write file: " + e.getMessage());
        }
    }
}

Output when successful: File written successfully

Important: The relative path sample.txt is resolved against the program's working directory, which may differ from the directory containing the Java source file.

5.4 Read a File

Files.readString() is useful when the complete text file is reasonably small and can fit into memory.

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

public class ReadFileExample {
    public static void main(String[] args) {
        Path path = Path.of("sample.txt");

        try {
            String content = Files.readString(
                path,
                StandardCharsets.UTF_8
            );

            System.out.println(content);
        } catch (IOException e) {
            System.err.println("Unable to read file: " + e.getMessage());
        }
    }
}

Possible output: Welcome to Core Java file handling.

If the file is missing, unreadable, or unavailable, the operation may throw an IOException or a more specific subclass such as NoSuchFileException.

5.5 Append Content to an Existing File

Appending adds text to the end of a file instead of replacing its existing content.

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;

public class AppendFileExample {
    public static void main(String[] args) {
        Path path = Path.of("activity.log");

        try {
            Files.writeString(
                path,
                "Application started" + System.lineSeparator(),
                StandardCharsets.UTF_8,
                StandardOpenOption.CREATE,
                StandardOpenOption.APPEND
            );

            System.out.println("Log entry appended");
        } catch (IOException e) {
            System.err.println("Append failed: " + e.getMessage());
        }
    }
}

The CREATE option creates the file if necessary, while APPEND requests writing at the end. If multiple threads or processes write to the same file, additional coordination may be needed to prevent interleaved or inconsistent records.

5.6 Read a Large Text File Line by Line

Reading a very large file all at once may consume too much memory. A buffered line-by-line approach processes the file incrementally.

import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

public class ReadLargeFile {
    public static void main(String[] args) {
        Path path = Path.of("large-data.txt");

        try (BufferedReader reader =
                 Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
            String line;

            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            System.err.println("File processing failed: " + e.getMessage());
        }
    }
}

Key concept: The try-with-resources statement closes the reader automatically, even if an exception occurs while processing the file.

5.7 Copy a File

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;

public class CopyFileExample {
    public static void main(String[] args) {
        Path source = Path.of("sample.txt");
        Path destination = Path.of("sample-copy.txt");

        try {
            Files.copy(
                source,
                destination,
                StandardCopyOption.REPLACE_EXISTING
            );

            System.out.println("File copied successfully");
        } catch (IOException e) {
            System.err.println("Copy failed: " + e.getMessage());
        }
    }
}

The replacement option allows an existing destination file to be replaced. A copy may fail due to permissions, missing source files, unavailable storage, or other file-system conditions.

6. File Exception Handling and Resource Management

6.1 Common File-Related Exceptions

Exception Typical Cause Recommended Response
IOException A general input/output failure Report the problem and handle or propagate it appropriately.
NoSuchFileException A requested path does not exist Verify the path and whether the file should be created.
AccessDeniedException Access to a file or directory is denied Check permissions and ownership.
FileAlreadyExistsException An operation expects a new path, but the path already exists Choose a different name or an appropriate replacement strategy.
SecurityException A security check prevents an operation Review the application's security context and permissions.

6.2 Why Use try-with-resources?

File readers, writers, and streams often use operating-system resources. These resources must be closed when they are no longer needed. Try-with-resources automatically closes objects that implement AutoCloseable.

import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

public class ResourceManagement {
    public static void main(String[] args) {
        Path path = Path.of("report.txt");

        try (BufferedWriter writer =
                 Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
            writer.write("Report generated successfully.");
            writer.newLine();
            writer.write("Resources will close automatically.");
        } catch (IOException e) {
            System.err.println("Unable to create report: " + e.getMessage());
        }
    }
}

This pattern avoids many resource leaks and is generally preferable to manually closing resources in a finally block.

6.3 Scenario: The File Is Not Found

Problem: A program tries to read a file, but it fails with a file-not-found-related exception.

Debugging steps:

  1. Print or inspect the resolved path.
  2. Confirm the file exists in the expected directory.
  3. Check the application's working directory.
  4. Check file permissions and spelling.
  5. Handle the failure with a clear message rather than silently ignoring it.
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class PathDiagnostic {
    public static void main(String[] args) {
        Path path = Path.of("input.txt");

        System.out.println("Absolute path: " + path.toAbsolutePath());
        System.out.println("Exists: " + Files.exists(path));
        System.out.println("Readable: " + Files.isReadable(path));

        try {
            String text = Files.readString(path);
            System.out.println(text);
        } catch (IOException e) {
            System.err.println(
                "Could not read " + path.toAbsolutePath() +
                ": " + e.getMessage()
            );
        }
    }
}

Note: The checks are useful for diagnosis, but file-system state or permissions may change between a check and the subsequent read. Always handle exceptions from the actual operation.

7. Debugging in Java

7.1 What Is Debugging?

Debugging is the process of locating, understanding, and correcting defects in software. A debugger lets you pause program execution, inspect variables, step through statements, and examine the call stack.

Debugging is not limited to syntax errors. It is also used to diagnose incorrect calculations, unexpected null values, file failures, race conditions, deadlocks, and resource-management issues.

7.2 Common Types of Errors

Error Type Description Example
Compile-time error The source code violates Java language or type rules. Calling a method with incompatible arguments.
Runtime exception An exceptional condition occurs during execution. Accessing a method through a null reference.
Logical error The program executes but produces an incorrect result. Using the wrong comparison operator.
Concurrency bug The result depends on timing or thread coordination. A race condition or deadlock.
Resource error A file or other resource cannot be used as expected. Reading a missing file or failing to close a stream.

7.3 Using Breakpoints

A breakpoint pauses a program at a selected line so that you can inspect its state. In IDEs such as IntelliJ IDEA, Eclipse, or NetBeans, breakpoints are typically set by clicking beside the line number.

  1. Open the Java source file in your IDE.
  2. Place a breakpoint near the statement where the problem appears.
  3. Start the application in Debug mode.
  4. When execution pauses, inspect local variables and object fields.
  5. Use Step Over to execute the current line without entering a method.
  6. Use Step Into to enter a method call.
  7. Use Step Out to finish the current method and return to its caller.
  8. Continue execution and verify whether the observed state matches your expectations.

7.4 Debugging a Logical Error

Problem: The program calculates the average of three numbers incorrectly because integer division discards the fractional part.

public class AverageDebug {
    public static void main(String[] args) {
        int a = 10;
        int b = 15;
        int c = 20;

        double average = (a + b + c) / 3;
        System.out.println(average);
    }
}

Output: 15.0

This example happens to produce an integer result, so it does not reveal the problem. Change the values to 10, 15, and 22. The integer division result is 15, even though the mathematical average is approximately 15.67.

Corrected code:

public class AverageFixed {
    public static void main(String[] args) {
        int a = 10;
        int b = 15;
        int c = 22;

        double average = (a + b + c) / 3.0;
        System.out.println(average);
    }
}

Output: 15.666666666666666

Converting the divisor to 3.0 makes the division floating-point arithmetic rather than integer arithmetic.

7.5 Reading a Stack Trace

A stack trace identifies the exception type, message, and the sequence of method calls active when the failure occurred.

Exception in thread "main" java.lang.NullPointerException
    at Example.process(Example.java:12)
    at Example.main(Example.java:5)

This illustrative stack trace indicates a NullPointerException. The first application frame points to line 12 of Example.java, where the failure occurred. Inspect the expression on that line and determine which reference may be null.

Useful debugging steps include:

  • Read the exception type and message first.
  • Locate the first stack frame belonging to your own code.
  • Inspect variables and method arguments at that location.
  • Reproduce the failure with a small test case.
  • Fix the underlying cause instead of merely hiding the exception.

7.6 Using Logging for Debugging

Logging records events and diagnostic information while an application runs. It is especially useful when a problem occurs intermittently or when a debugger cannot conveniently pause the application.

import java.util.logging.Level;
import java.util.logging.Logger;

public class LoggingExample {
    private static final Logger LOGGER =
        Logger.getLogger(LoggingExample.class.getName());

    public static void main(String[] args) {
        int value = 10;
        LOGGER.info("Starting calculation");

        try {
            int result = 100 / value;
            LOGGER.info("Result: " + result);
        } catch (ArithmeticException e) {
            LOGGER.log(Level.SEVERE, "Calculation failed", e);
        }
    }
}

Use appropriate logging levels such as INFO, WARNING, and SEVERE. Avoid logging passwords, access tokens, private user data, or other sensitive information.

8. Solving Combined Real-World Scenarios

8.1 Scenario: Multiple Threads Need to Write to One File

Problem: Several worker threads append log messages to the same file. Lines may become difficult to interpret or writes may conflict, depending on the APIs and access pattern.

Approach: Use one dedicated writer thread or a synchronized logging component to coordinate file access. For a simple example, a synchronized method serializes writes through a single shared writer.

import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;

class SharedFileLogger implements AutoCloseable {
    private final BufferedWriter writer;

    public SharedFileLogger(Path path) throws IOException {
        writer = Files.newBufferedWriter(
            path,
            StandardCharsets.UTF_8,
            StandardOpenOption.CREATE,
            StandardOpenOption.APPEND
        );
    }

    public synchronized void log(String message) throws IOException {
        writer.write(
            Thread.currentThread().getName() + ": " + message
        );
        writer.newLine();
        writer.flush();
    }

    @Override
    public void close() throws IOException {
        writer.close();
    }
}

public class SharedFileLoggerExample {
    public static void main(String[] args) throws IOException {
        try (SharedFileLogger logger =
                 new SharedFileLogger(Path.of("application.log"))) {

            Runnable task = () -> {
                try {
                    logger.log("Task started");
                    logger.log("Task completed");
                } catch (IOException e) {
                    System.err.println("Logging failed: " + e.getMessage());
                }
            };

            Thread t1 = new Thread(task, "Worker-1");
            Thread t2 = new Thread(task, "Worker-2");

            t1.start();
            t2.start();

            try {
                t1.join();
                t2.join();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                System.err.println("Main thread interrupted");
            }
        }
    }
}

Important design considerations:

  • The synchronized method prevents concurrent calls through this logger instance from interleaving their write operations.
  • Only close the writer after all worker threads have finished using it.
  • In production code, consider a dedicated logging framework or a queue-based writer service.
  • Do not assume synchronization on one Java object coordinates other logger instances or other processes writing to the same file.
  • Define how logging failures should be reported and whether the application should continue.

8.2 Scenario: A Background File Task Must Not Block the Main Thread

Problem: Reading a large file directly in the main thread can delay other work. Move the I/O operation into an executor task when asynchronous processing is appropriate.

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class BackgroundFileRead {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newSingleThreadExecutor();

        try {
            Future<String> future = executor.submit(
                () -> Files.readString(Path.of("input.txt"))
            );

            System.out.println("File reading task submitted");

            try {
                String content = future.get();
                System.out.println("Characters read: " + content.length());
            } catch (Exception e) {
                System.err.println("File task failed: " + e.getMessage());
            }
        } finally {
            executor.shutdown();
        }
    }
}

Explanation: The file-reading task executes through the executor, while the main thread can perform other work before calling future.get(). The call to get() waits for the result, so this particular example still blocks at that point. The checked IOException from the file operation is propagated through the task and may be wrapped in an ExecutionException.

For a fully asynchronous workflow, attach a completion stage or otherwise process the result without immediately waiting on get(). In production code, also define shutdown, cancellation, and error-handling behaviour.

8.3 Scenario: An Application Hangs Unexpectedly

When a Java application appears frozen, investigate whether the main thread is waiting for a worker, a thread is blocked on a lock, a task is waiting forever, or the application is performing unusually expensive work.

  1. Reproduce the hang with the same input.
  2. Capture a thread dump using the IDE or JVM diagnostic tools.
  3. Inspect thread states and stack traces.
  4. Look for threads waiting on locks or waiting for one another.
  5. Check whether a lock-order cycle suggests deadlock.
  6. Inspect loops, blocking I/O, and long-running computations.
  7. Apply a fix and repeat the test to verify the issue is resolved.

9. Best Practices

Multithreading

  • Prefer executors and higher-level concurrency utilities for managing groups of tasks.
  • Protect shared mutable state with synchronization, locks, atomic classes, or thread-safe data structures as appropriate.
  • Avoid acquiring multiple locks in inconsistent orders.
  • Use interruption cooperatively and restore the interrupt status when needed.
  • Do not depend on a particular thread execution order unless you explicitly coordinate it.
  • Write tests that exercise concurrent access and repeat them to expose timing-dependent problems.

File Handling

  • Prefer Path and Files for modern file-system operations.
  • Use try-with-resources for resources that implement AutoCloseable.
  • Specify a character encoding such as UTF-8 when reading or writing text.
  • Handle expected I/O failures with useful error messages.
  • Read very large files incrementally instead of loading them completely into memory.
  • Validate paths and permissions, especially when paths come from external input.
  • Do not assume that checking whether a file exists guarantees that the next operation will succeed.

Debugging

  • Reproduce the problem and create the smallest useful test case.
  • Read exception messages and stack traces carefully.
  • Use breakpoints and inspect variables instead of guessing.
  • Add meaningful logging around important operations.
  • Check boundary conditions, null references, and invalid input.
  • For concurrency bugs, inspect thread dumps and shared-state access.
  • After fixing a bug, add a regression test so it is less likely to return.

10. Practical Exercises

Beginner Exercises

  1. Create two threads that print different messages.
  2. Use join() to ensure a worker completes before the main thread continues.
  3. Write a Java program that creates a text file and writes three lines.
  4. Read a text file line by line and count its lines.
  5. Handle a missing file using an appropriate exception handler.
  6. Use a breakpoint to inspect a variable before and after a calculation.

Intermediate Exercises

  1. Implement a thread-safe counter and verify the final value after multiple workers finish.
  2. Create an executor that processes several independent tasks.
  3. Append timestamped entries to a log file.
  4. Copy a file and handle an existing destination path.
  5. Reproduce a logical error and use the debugger to identify the incorrect expression.
  6. Interrupt a sleeping worker and ensure the application exits cleanly.

Advanced Exercises

  1. Reproduce a deadlock with two locks, inspect it with a thread dump, and fix the lock ordering.
  2. Design a producer-consumer program where workers send log messages to one file-writing thread.
  3. Process a large text file in the background and report progress safely.
  4. Write tests for a concurrent counter and check for lost updates.
  5. Build a small file-processing utility with input validation, exception handling, logging, and graceful shutdown.

11. Interview Questions and Answers

Q1. What is multithreading in Java?

Answer: Multithreading allows multiple threads to make progress within the same process. It is useful for concurrent tasks, background work, and responsive applications.

Q2. What is the difference between start() and run()?

Answer: start() initiates a new thread of execution, which invokes run(). Calling run() directly executes the method on the current thread.

Q3. What is a race condition?

Answer: A race condition occurs when a program's correctness depends on the timing or ordering of concurrent operations, often because shared data is accessed without suitable coordination.

Q4. What is synchronization?

Answer: Synchronization coordinates access to shared data and establishes memory-visibility guarantees. Java's synchronized methods and blocks use object monitors to control access.

Q5. What is deadlock?

Answer: Deadlock is a condition in which two or more threads are permanently waiting for resources held by one another. Consistent lock ordering and reducing nested locking help prevent it.

Q6. Why is join() used?

Answer: The join() method lets one thread wait for another thread to terminate before proceeding.

Q7. What is try-with-resources?

Answer: It is a Java language feature that automatically closes resources implementing AutoCloseable when the try block finishes, including when an exception occurs.

Q8. What is the difference between byte streams and character streams?

Answer: Byte streams process binary data, while character streams process text through character encoding and decoding. Choose the API that matches the data being handled.

Q9. What is a breakpoint?

Answer: A breakpoint pauses program execution at a selected location so the developer can inspect variables, evaluate expressions, and step through the program.

Q10. How do you debug a NullPointerException?

Answer: Read the stack trace, locate the failing line, inspect the references used in that expression, and determine why a required reference is null. Correct the underlying logic and add appropriate validation or tests.

Q11. How do you debug a Java application that hangs?

Answer: Capture a thread dump, examine thread states and stack traces, look for lock cycles or blocking operations, and inspect long-running loops and I/O.

Q12. Why should file resources be closed?

Answer: Closing files releases operating-system resources and helps ensure data is flushed and resources are not leaked. Try-with-resources is usually the safest straightforward approach.

12. Quick Revision Checklist

  • Understand how threads are created and started.
  • Know the difference between concurrency, race conditions, synchronization, and deadlocks.
  • Use join() and interruption appropriately.
  • Understand when an executor is useful.
  • Use Path and Files for common file operations.
  • Handle I/O exceptions and close resources with try-with-resources.
  • Understand the difference between byte-oriented and character-oriented I/O.
  • Use breakpoints, stepping, variable inspection, and stack traces.
  • Use logging to investigate intermittent problems.
  • Test the fix and add regression tests when possible.

13. Conclusion

Solving multithreading, file handling, and debugging scenarios develops practical skills needed to build reliable Core Java applications. Multithreading helps coordinate concurrent work, file handling makes persistent data operations possible, and debugging provides a systematic way to identify and correct defects.

To improve, practise each topic independently before combining them in small projects. Pay particular attention to thread coordination, resource cleanup, exception handling, and repeatable debugging techniques. These habits help produce Java applications that are easier to maintain, test, and troubleshoot.

Learn more through the JustAcademy Core Java Training course, register for a course demo, or visit the JustAcademy website.

Previous Next
whatsapp